<?php
/**
 * crcl/base
 *
 * Copyright 2013 CRCL - André Sabosch - info@crcl.de
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace crcl\base\exception;

interface IData
{
  /**
   * Collects data from exception and returns data object // factory
   *
   * @static
   *
   * @param exception $e
   *
   * @return \crcl\base\exception\Data
   */
  public static function fromException($e);

  /**
   * Collects data from error (callback) and returns data object // factory
   *
   * @static
   *
   * @param int     $iError   Code
   * @param string  $sError   Message
   * @param string  $sFile    Filename and path
   * @param int     $iLine    Line number
   * @param array   $aContext Context array
   *
   * @return \crcl\base\exception\Data
   */
  public static function fromError($iError, $sError, $sFile, $iLine = \NULL, $aContext = \NULL);

  /**
   * Setter for class where error occured. If no class exists (error) you could use type of error
   *
   * @param string  $sClass Class or error type
   */
  public function setClass($sClass);

  /**
   * Class getter
   *
   * @return string
   */
  public function getClass();

  /**
   * Setter for error message
   *
   * @param string  $sMessage
   */
  public function setMessage($sMessage);

  /**
   * Error message getter
   *
   * @return string
   */
  public function getMessage();

  /**
   * Setter for error code
   *
   * @param int $iCode
   */
  public function setCode($iCode);

  /**
   * Error code getter
   *
   * @return int
   */
  public function getCode();

  /**
   * Setter for file where error occured
   *
   * @param string  $sFile
   */
  public function setFile($sFile);

  /**
   * File getter
   *
   * @return string
   */
  public function getFile();

  /**
   * Setter for line where error occured
   *
   * @param int $iLine
   */
  public function setLine($iLine);

  /**
   * Line getter
   *
   * @return int
   */
  public function getLine();

  /**
   * Setter for trace
   *
   * @param array $aTrace
   */
  public function setTrace($aTrace);

  /**
   * Trace getter
   *
   * @return  array
   */
  public function getTrace();

  /**
   * Setter for causes (crcl\base\IException only)
   *
   * @param \crcl\base\exception\ICauses|NULL $oCauses
   */
  public function setCauses($oCauses);

  /**
   * Causes getter
   *
   * @return \crcl\base\exception\ICauses|NULL
   */
  public function getCauses();

  /**
   * Datetime getter
   * datetime is automatically set on object creation
   *
   * @return datetime
   */
  public function getThrown();

  /**
   * info array getter
   * info array is automatically set on object creation
   *
   * @return  array
   */
  public function getInfos();

  /**
   * Setter for context array
   *
   * @param array $aContext
   */
  public function setContext($aContext);

  /**
   * Context getter
   * @return  array
   */
  public function getContext();

  /**
   * HTTP Status getter
   * @return int
   */
  public function getStatus();

  /**
   * Returns HTTP Status error message
   *
   * @return  string
   */
  public function getStatusError();

  /**
   * Returns HTTP Status error description
   *
   * @return  string
   */
  public function getStatusDescription();

  /**
   * HTTP Status setter
   *
   * @param int $iStatus
   */
  public function setStatus($iStatus);
}

class Data implements IData
{
  const TYPE_ERROR = 'Error';
  const TYPE_WARNING = 'Warning';
  const TYPE_PARSE = 'Parse error';
  const TYPE_NOTICE = 'Notice';
  const TYPE_CORE_ERROR = 'Core error';
  const TYPE_CORE_WARNING = 'Core warning';
  const TYPE_COMPILE_ERROR = 'Compile error';
  const TYPE_COMPILE_WARNING = 'Compile warning';
  const TYPE_USER_ERROR = 'User error';
  const TYPE_USER_WARNING = 'User warning';
  const TYPE_USER_NOTICE = 'User notice';
  const TYPE_STRICT = 'Strict notice';
  const TYPE_RECOVERABLE_ERROR = 'Recoverable error';
  const TYPE_DEPRECATED = 'Deprecated error';
  const TYPE_USER_DEPRECATED = 'User deprecated error';

  public function __construct()
  {
    $this->dtThrown = date('Y-m-d H:i:s');
    $this->aInfos = $this->collectInfos();
  }

  public static function fromException($e)
  {
    $oData = new Data();

    //exception classics
    $oData->setClass(\get_class($e));
    $oData->setMessage($e->getMessage());
    $oData->setCode($e->getCode());
    $oData->setFile($e->getFile());
    $oData->setLine($e->getLine());

    //trace as array
    $aTrace = $e->getTraceAsString();
    $aTrace = \explode("\n", $aTrace);
    $oData->setTrace($aTrace);

    //causes and status only exists in our exceptions
    if ($e instanceof \crcl\base\IException)
    {
      $oData->setCauses($e->getCauses());
      $oData->setStatus($e->getStatus());
    }

    return $oData;
  }

  public static function fromError($iError, $sError, $sFile, $iLine = \NULL, $aContext = array())
  {
    $oData = new Data();
    $sType = self::evaluateErrortype($iError);
    $oData->setClass($sType);
    $oData->setCode($iError);
    $oData->setMessage($sError);
    $oData->setFile($sFile);
    $oData->setLine($iLine);
    $oData->setContext($aContext);

    $aTrace = self::evaluateBacktrace();
    $oData->setTrace($aTrace);

    return $oData;
  }

  public function setClass($sClass)
  {
    $this->sClass = $sClass;
  }

  public function getClass()
  {
    return $this->sClass;
  }

  public function setMessage($sMessage)
  {
    $this->sMessage = $sMessage;
  }

  public function getMessage()
  {
    return $this->sMessage;
  }

  public function setCode($iCode)
  {
    $this->iCode = $iCode;
  }

  public function getCode()
  {
    return $this->iCode;
  }

  public function setFile($sFile)
  {
    $this->sFile = $sFile;
  }

  public function getFile()
  {
    return $this->sFile;
  }

  public function setLine($iLine)
  {
    $this->iLine = $iLine;
  }

  public function getLine()
  {
    return $this->iLine;
  }

  public function setTrace($aTrace)
  {
    $this->aTrace = $aTrace;
  }

  public function getTrace()
  {
    return $this->aTrace;
  }

  public function setCauses($oCauses)
  {
    $this->oCauses = $oCauses;
  }

  public function getCauses()
  {
    return $this->oCauses;
  }

  public function getThrown()
  {
    return $this->dtThrown;
  }

  public function getInfos()
  {
    return $this->aInfos;
  }

  public function setContext($aContext)
  {
    if (!\is_array($aContext))
    {
      $aContext = array($aContext);
    }
    $this->aContext = $aContext;
  }

  public function getContext()
  {
    $aContext = $this->getSafeContext();
    return $aContext;
  }

  public function setStatus($iStatus)
  {
    $this->iStatus = $iStatus;
  }

  public function getStatus()
  {
    return $this->iStatus;
  }

  public function getStatusError()
  {
    if (isset($this->aStatusError[$this->iStatus]))
    {
      return $this->aStatusError[$this->iStatus];
    }
    return '';
  }

  public function getStatusDescription()
  {
    if (isset($this->aStatusDescription[$this->iStatus]))
    {
      return $this->aStatusDescription[$this->iStatus];
    }
    return '';
  }

  /* PRIVATE **************************************************************************************/

  private static $aInfosSources = array('_GET', '_POST', '_SESSION', '_SERVER');
  private $dtThrown;
  private $sClass = \NULL;
  private $sMessage = \NULL;
  private $iCode = \NULL;
  private $sFile = \NULL;
  private $iLine = \NULL;
  private $aTrace = array();
  private $oCauses = \NULL;
  private $aInfos = array();
  private $iStatus = \crcl\base\Exception::STATUS_DEFAULT;
  private $aContext = array();
  private $aStatusError = array(
    404 => 'Not Found.',
    500 => 'Internal Server Error');
  private $aStatusDescription = array(
    404 => 'The page that you have requested could not be found.',
    500 => 'The server encountered an internal error and was unable to complete your request.');

  private static function evaluateErrortype($iError)
  {
    switch ($iError)
    {
      case E_ERROR:
        return self::TYPE_ERROR;
        break;
      case E_WARNING:
        return self::TYPE_WARNING;
        break;
      case E_PARSE:
        return self::TYPE_PARSE;
        break;
      case E_NOTICE:
        return self::TYPE_NOTICE;
        break;
      case E_CORE_ERROR:
        return self::TYPE_CORE_ERROR;
        break;
      case E_CORE_WARNING:
        return self::TYPE_COMPILE_WARNING;
        break;
      case E_COMPILE_ERROR:
        return self::TYPE_COMPILE_ERROR;
        break;
      case E_COMPILE_WARNING:
        return self::TYPE_COMPILE_WARNING;
        break;
      case E_USER_ERROR:
        return self::TYPE_USER_ERROR;
        break;
      case E_USER_WARNING:
        return self::TYPE_USER_WARNING;
        break;
      case E_USER_NOTICE:
        return self::TYPE_USER_NOTICE;
        break;
      case E_STRICT:
        return self::TYPE_STRICT;
        break;
      case E_RECOVERABLE_ERROR:
        return self::TYPE_RECOVERABLE_ERROR;
        break;
      case E_DEPRECATED:
        return self::TYPE_DEPRECATED;
        break;
      case E_USER_DEPRECATED:
        return self::TYPE_USER_DEPRECATED;
        break;
    }
    return 'Unknown error (' . $iError . ')';
  }

  private static function evaluateBacktrace()
  {
    \ob_start();
    \debug_print_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
    $aTrace = \ob_get_clean();

    $aTrace = \explode("\n", $aTrace);

    return $aTrace;
  }

  private static function getSafeSimpleHtmlDom($context)
  {
    if (\is_object($context))
    {
      $sClass = \get_class($context);
      if ($sClass == 'simple_html_dom' || $sClass == 'simple_html_dom_node')
      {
        \ob_start();
        if (!\is_null($context->root))
        {
          $context->dump();
        }
        $context = \ob_get_clean();
      }
    }
    return $context;
  }

  private function collectInfos()
  {
    $aInfos = array();
    $aInfos['include_path'] = \ini_get('include_path');

    foreach (self::$aInfosSources as $source)
    {
      if (isset($GLOBALS[$source]))
      {
        if (!empty($GLOBALS[$source]))
        {
          $aInfos[$source] = $GLOBALS[$source];
        }
      }
    }
    return $aInfos;
  }

  private function getSafeContext()
  {
    $aContext = $this->aContext;

    //simple_html objects are recursive... producing out of memory errors on printing
    //todo automatic detection of recursive problems?
    //https://code.google.com/p/crcl-base/issues/detail?id=3
    foreach ($aContext as $key => $context)
    {
      $aContext[$key] = self::getSafeSimpleHtmlDom($context);
      if (\is_array($context))
      {
        foreach ($context as $innerkey => $item)
        {
          $context[$innerkey] = self::getSafeSimpleHtmlDom($item);
        }
        $aContext[$key] = $context;
      }
    }
    return $aContext;
  }
}